/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.force4spring;

import java.rmi.RemoteException;

import org.force4spring.support.ForceSession;
import org.force4spring.support.SessionFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.ResourceTransactionManager;
import org.springframework.transaction.support.SmartTransactionObject;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import com.sforce.soap.partner.fault.ApiFault;

/**
 * {@link org.springframework.transaction.PlatformTransactionManager}
 * implementation for a single Force.com SessionFactory. Binds a Force.com Session from
 * the specified SessionFactory to the thread, potentially allowing for one
 * thread-bound Session per SessionFactory. {@link SessionFactoryUtils} and
 * {@link ForceTemplate} are aware of thread-bound ForceSessions and participate
 * in such transactions automatically. Using either of those is required for
 * Force.com access code that needs to support this transaction handling mechanism.
 * 
 * @author Juergen Hoeller
 * @author Max Rudman
 * @since 09.01.2007
 * @see #setSessionFactory
 * @see #setDataSource
 * @see SessionFactoryUtils#getSession
 * @see SessionFactoryUtils#applyTransactionTimeout
 * @see SessionFactoryUtils#releaseSession
 * @see ForceTemplate#execute
 * @see org.springframework.transaction.jta.JtaTransactionManager
 */
public class ForceTransactionManager extends AbstractPlatformTransactionManager
		implements ResourceTransactionManager, InitializingBean {

	private SessionFactory sessionFactory;

	/**
	 * Create a new HibernateTransactionManager instance. A SessionFactory has
	 * to be set to be able to use it.
	 * 
	 * @see #setSessionFactory
	 */
	public ForceTransactionManager() {
	}

	/**
	 * Create a new HibernateTransactionManager instance.
	 * 
	 * @param sessionFactory
	 *            SessionFactory to manage transactions for
	 */
	public ForceTransactionManager(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
		afterPropertiesSet();
	}

	/**
	 * Set the SessionFactory that this instance should manage transactions for.
	 */
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	/**
	 * Return the SessionFactory that this instance should manage transactions
	 * for.
	 */
	public SessionFactory getSessionFactory() {
		return this.sessionFactory;
	}

	public void afterPropertiesSet() {
		if (getSessionFactory() == null) {
			throw new IllegalArgumentException(
					"Property 'sessionFactory' is required");
		}
	}

	public Object getResourceFactory() {
		return getSessionFactory();
	}

	protected Object doGetTransaction() {
		ApexTransactionObject txObject = new ApexTransactionObject();

		SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
				.getResource(getSessionFactory());
		if (sessionHolder != null) {
			if (logger.isDebugEnabled()) {
				logger
						.debug("Found thread-bound Session ["
								+ sessionHolder.getSession()
								+ "] for Apex transaction");
			}
			txObject.setSessionHolder(sessionHolder, false);
		}
		
		return txObject;
	}
	
	protected boolean isExistingTransaction(Object transaction) {
		return ((ApexTransactionObject) transaction).hasTransaction();
	}

	protected void doBegin(Object transaction, TransactionDefinition definition) {
		ApexTransactionObject txObject = (ApexTransactionObject) transaction;
		
		ForceSession session = null;

		try {
			if (txObject.getSessionHolder() == null
					|| txObject.getSessionHolder()
							.isSynchronizedWithTransaction()) {
				ForceSession newSession = sessionFactory
						.openSession();
				if (logger.isDebugEnabled()) {
					logger.debug("Opened new Session [" + newSession
							+ "] for Apex transaction");
				}
				txObject.setSessionHolder(new SessionHolder(newSession), true);
			}

			txObject.getSessionHolder().startTransaction();
			txObject.getSessionHolder().setSynchronizedWithTransaction(true);
			session = txObject.getSessionHolder().getSession();

			// Register transaction timeout.
			int timeout = determineTimeout(definition);
			if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
				txObject.getSessionHolder().setTimeoutInSeconds(timeout);
			}

			// Bind the session holder to the thread.
			if (txObject.isNewSessionHolder()) {
				TransactionSynchronizationManager.bindResource(
						getSessionFactory(), txObject.getSessionHolder());
			}
		} catch (Exception ex) {
			SessionFactoryUtils.closeSession(session);
			throw new CannotCreateTransactionException(
					"Could not open Force Session for transaction", ex);
		}
	}

	protected Object doSuspend(Object transaction) {
		ApexTransactionObject txObject = (ApexTransactionObject) transaction;
		txObject.setSessionHolder(null, false);
		SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
				.unbindResource(getSessionFactory());
		return new SuspendedResourcesHolder(sessionHolder);
	}

	protected void doResume(Object transaction, Object suspendedResources) {
		SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources;
		if (TransactionSynchronizationManager.hasResource(getSessionFactory())) {
			// From non-transactional code running in active transaction
			// synchronization
			// -> can be safely removed, will be closed on transaction
			// completion.
			TransactionSynchronizationManager
					.unbindResource(getSessionFactory());
		}
		TransactionSynchronizationManager.bindResource(getSessionFactory(),
				resourcesHolder.getSessionHolder());
	}

	protected void doCommit(DefaultTransactionStatus status) {
		ApexTransactionObject txObject = (ApexTransactionObject) status
				.getTransaction();
		if (status.isDebug()) {
			logger.debug("Committing Apex transaction on Session ["
					+ txObject.getSessionHolder().getSession() + "]");
		}
		try {
			txObject.getSessionHolder().getSession().flush();
		} catch (ApiFault fault) {
			SessionFactoryUtils.convertApiFault(fault);
		} catch (RemoteException re) {
			throw new WebServicesCallException(re);
		}
	}

	protected void doRollback(DefaultTransactionStatus status) {
		ApexTransactionObject txObject = (ApexTransactionObject) status
				.getTransaction();
		if (status.isDebug()) {
			logger.debug("Rolling back Force transaction on Session ["
					+ txObject.getSessionHolder().getSession() + "]");
		}
		txObject.getSessionHolder().getSession().clear();
	}

	protected void doSetRollbackOnly(DefaultTransactionStatus status) {
		ApexTransactionObject txObject = (ApexTransactionObject) status
				.getTransaction();
		if (status.isDebug()) {
			logger.debug("Setting Force transaction on Session ["
					+ txObject.getSessionHolder().getSession()
					+ "] rollback-only");
		}
		txObject.setRollbackOnly();
	}

	protected void doCleanupAfterCompletion(Object transaction) {
		ApexTransactionObject txObject = (ApexTransactionObject) transaction;
		
		// Remove the session holder from the thread.
		if (txObject.isNewSessionHolder()) {
			TransactionSynchronizationManager
					.unbindResource(getSessionFactory());
		}
		
		ForceSession session = txObject.getSessionHolder().getSession();
		if (txObject.isNewSessionHolder()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Closing Force Session [" + session + "] after transaction");
			}
			SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, getSessionFactory());
		}
		else {
			if (logger.isDebugEnabled()) {
				logger.debug("Not closing pre-bound Force Session [" + session + "] after transaction");
			}
		}

		txObject.getSessionHolder().clear();
	}

	/**
	 * Hibernate transaction object, representing a SessionHolder. Used as
	 * transaction object by HibernateTransactionManager.
	 * 
	 * @see SessionHolder
	 */
	private static class ApexTransactionObject implements
			SmartTransactionObject {

		private SessionHolder sessionHolder;

		private boolean newSessionHolder;

		public void setSessionHolder(SessionHolder sessionHolder,
				boolean newSessionHolder) {
			this.sessionHolder = sessionHolder;
			this.newSessionHolder = newSessionHolder;
		}
		
		public boolean hasTransaction() {
			return (this.sessionHolder != null) && (this.sessionHolder.isTransactionStarted());
		}

		public SessionHolder getSessionHolder() {
			return this.sessionHolder;
		}

		public boolean isNewSessionHolder() {
			return this.newSessionHolder;
		}

		public void setRollbackOnly() {
			getSessionHolder().setRollbackOnly();
		}

		public boolean isRollbackOnly() {
			return getSessionHolder().isRollbackOnly();
		}
	}

	/**
	 * Holder for suspended resources. Used internally by <code>doSuspend</code>
	 * and <code>doResume</code>.
	 */
	private static class SuspendedResourcesHolder {

		private final SessionHolder sessionHolder;

		private SuspendedResourcesHolder(SessionHolder sessionHolder) {
			this.sessionHolder = sessionHolder;
		}

		private SessionHolder getSessionHolder() {
			return this.sessionHolder;
		}
	}
}
